﻿using SIR.Common;
using SIR.Common.Condition;
using SIR.Entities.Implementations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;

namespace SIR.Web.API
{
	/// <summary>
	/// WebAPI controller to download and upload data to the system
	/// </summary>
	public class DataController : ApiController
	{
		/// <summary>
		/// Creates a response based on the specified criteria
		/// </summary>
		/// <param name="criteria">The criteria to download data</param>
		/// <returns>A response containing success, any message, and the results</returns>
		public Response<IEnumerable<IEntity>> Download(DownloadCriteria criteria)
		{
			// Parse the predicate and download based on object name
			return Download(criteria.objectName, PredicateGroup.Parse(criteria.condition));
		}

		/// <summary>
		/// Returns a response containing a list of objects meeting the condition
		/// </summary>
		/// <param name="objectName">The types of objets to retrieve</param>
		/// <param name="condition">The conditiion to use to read the data</param>
		/// <returns>A response containing success, any message, and the results</returns>
		public Response<IEnumerable<IEntity>> Download(string objectName, IPredicate condition)
		{
			// Get the type of entity from the object name
			var objectType = GetEntityType(objectName);
			var requireAuthentication = true;
			// Check to see if the object can be downloaded and if it requires authentication first
			if (objectType == null || !_allowedDownload.TryGetValue(objectType, out requireAuthentication))
				return Response<IEnumerable<IEntity>>.CreateResponse(false, null, "Invalid request");
			// Requires authentication before download?
			if (requireAuthentication)
			{
				long? userId;
				// Check the sesssion for the authenticated user
				SessionManager.GetSession(HttpContext.Current, out userId, false);
				// User not authentication, download denied
				if (!userId.HasValue)
					return Response<IEnumerable<IEntity>>.CreateResponse(false, null, "User must be authenticated");
				// Create a condition for user predicate
				var userCondition = new Predicate(objectType == typeof(User) ? "Id" : "UserId", userId.Value, PredicateOperator.EqualTo);
				if (condition == null) // If no condition then use the user condition
					condition = userCondition;
				else if (!ContainsUserCondition(objectName, condition)) // Already a condition so create a new predicate group
					condition = new PredicateGroup(new[] { userCondition, condition }, true);
			}
			// If looking for favorite images, find images using the favorite images as the joining table 
			var results = objectType == typeof(FavoriteImage) ?
				DAL.DataServer.FindByJoin(typeof(Image), objectType, condition) :
				DAL.DataServer.Find(objectType, condition);
			// Return the results
			return Response<IEnumerable<IEntity>>.CreateResponse(results.WasSuccessful, results.Results, results.ErrorMessage);
		}

		/// <summary>
		/// Get the entity type from the object name
		/// </summary>
		/// <param name="objectName">The name of the object</param>
		/// <returns>The type of the entity by the name name</returns>
		private static Type GetEntityType(string objectName)
		{
			var objectType = System.Reflection.Assembly.Load("SIR.Entities.Implementations").GetType("SIR.Entities.Implementations." + objectName);
			return objectType;
		}
		/// <summary>
		/// Check to see if the predicate already contains a user condition
		/// </summary>
		/// <param name="objectName">Target object name</param>
		/// <param name="condition">The condition</param>
		/// <returns>True if the condition contains a user clause</returns>
		private static bool ContainsUserCondition(string objectName, IPredicate condition)
		{
			// No condition, then no user condition
			if (condition == null)
				return false;
			// Check if the condition contains an Id key (for user) or userId key
			return condition.ContainsKey(objectName == typeof(User).Name ? "Id" : "UserId");
		}

		private static Dictionary<Type, bool> _allowedDownload = new Dictionary<Type, bool>()
		{	// Type, Require Authentication
			{ typeof(User), true },
			{ typeof(Folder), true },
			{ typeof(Image), true },
			{ typeof(FavoriteImage), true },
			{ typeof(PatternDescription), false },
			{ typeof(PatternDescriptionGroup), false }
		};

		private static Dictionary<Type, string[]> _allowedUpload = new Dictionary<Type, string[]>()
		{
			// Type, Fields that are allowed for upload
			{ typeof(Folder), new [] { "Id", "Name", "ParentFolderId", "IsDeleted", "IsPrivate" } },
			{ typeof(Image), new [] { "Id", "Name", "FolderId", "IsDeleted", "IsPrivate" } },
			{ typeof(FavoriteImage), new [] { "Id", "ImageId", "IsDeleted" } } 
		};
		/// <summary>
		/// Uploads changes to the data
		/// </summary>
		/// <param name="criteria">The object name and the changes to be made on the object</param>
		/// <returns>A response containing the new object values</returns>
		public Response<IEntity> Upload(UploadCriteria criteria)
		{
			var data = new Dictionary<string, string>();
			// Build a dictionary from the values to ensure no duplicates
			foreach (var kvp in criteria.values)
				data.Add(kvp.Key, kvp.Value);
			var objectType = criteria.objectType;

			long? userId;
			// Get the user from the sesssion
			SessionManager.GetSession(HttpContext.Current, out userId, false);
			// No authenticated user, fail the upload
			if (!userId.HasValue)
				return Response<IEntity>.CreateResponse(false, null, "User must be authenticated");
			// Get the entity type
			Type type = GetEntityType(objectType);
			// Type could not be found, type doesn't allow upload, or no values in the request, fail the upload
			if (type == null || !_allowedUpload.Keys.Contains(type) || data == null || data.Keys.Count < 1)
				return Response<IEntity>.CreateResponse(false, null, "Invalid request");

			IEntity entity = null;
			// Remove all disallowed fields
			var disallowed = data.Keys.Where(k => !_allowedUpload[type].Contains(k)).ToList();
			foreach (var key in disallowed)
				data.Remove(key);
			// If Id exists, then it is an update
			if (data.ContainsKey("Id"))
			{
				// must already exist for that user
				var existingItemResult = DAL.DataServer.Find(type, new PredicateGroup(new[] {
					new Predicate("Id", data["Id"], PredicateOperator.EqualTo),
					new Predicate("UserId", userId.Value, PredicateOperator.EqualTo)
				}, true));
				entity = existingItemResult.Results.FirstOrDefault();
				// Item could not be found, fail upload
				if (entity == null)
					return Response<IEntity>.CreateResponse(false, null, "Invalid request");
				// cannot change the ID
				data.Remove("Id");
			}
			else
			{
				bool parsedIsDeleted = false;
				// Deleting an item that doesn't exist, send success response
				if (data.ContainsKey("IsDeleted") && bool.TryParse(data["IsDeleted"], out parsedIsDeleted) && parsedIsDeleted)
					return Response<IEntity>.CreateResponse(true, null);
				// Create a new instance of the object
				entity = Activator.CreateInstance(type) as IEntity;
			}
			// Add UserId from the session to the list of fields to set
			data.Add("UserId", userId.Value.ToString());

			// Loop through the values in the criteria upload
			foreach (var kvp in data)
			{
				try
				{
					// Get the property by the key name
					var property = type.GetProperty(kvp.Key);

					if (property != null)
					{
						// Set the value
						property.SetValue(entity, Convert.ChangeType(kvp.Value, property.PropertyType));
					}
				}
				catch { }
			}

			// For images and folders, change to see if the name is duplicated
			if (type == typeof(Image) || type == typeof(Folder))
			{
				var nameProperty = type.GetProperty("Name");
				var folderIdProperty = type == typeof(Image) ? type.GetProperty("FolderId") : type.GetProperty("ParentFolderId");
				// Use the name resolver to ensure the name is unique per folder per user
				nameProperty.SetValue(entity, NameResolver.Resolve(type, userId.Value, entity.Id,
					folderIdProperty.GetValue(entity) as long?,
					nameProperty.GetValue(entity).ToString()));
			}
			// Save the updates
			var result = DAL.DataServer.Save(type, entity);
			// Send the response back with success and the updated object
			return Response<IEntity>.CreateResponse(result.WasSuccessful, result.NewValue, result.ErrorMessage);
		}
	}
	/// <summary>
	/// Criteria for uploading data
	/// </summary>
	public class UploadCriteria
	{
		/// <summary>
		/// Gets or sets the name of the object to upload data to
		/// </summary>
		public string objectType { get; set; }
		/// <summary>
		/// Gets or sets the values for the object to upload
		/// </summary>
		public List<KeyValuePair<string, string>> values { get; set; }
	}
	/// <summary>
	/// Criteria for downloading data
	/// </summary>
	public class DownloadCriteria
	{
		/// <summary>
		/// Gets or sets the name of the object to download the data
		/// </summary>
		public string objectName { get; set; }
		/// <summary>
		/// Gets or sets the condition to be met by the downloaded data
		/// </summary>
		public string condition { get; set; }
	}
}
